﻿using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;

namespace TrinityEngine
{
    public class SpriteBatcher : IDisposable
    {
        public static SpriteBatcher Instance;

        private bool _disposed = false;

        private List<BatchedSpriteRenderer> _rendererList = new List<BatchedSpriteRenderer>();

        private Texture[] _textures;

        private int _textureIndex = 0;

        private float[] _vertexData;
        // 3 position 2 texCoord 4 color 1 texID

        private int _vertexIndex = 0;

        private List<uint> _indexData = new List<uint>();

        private readonly Vector4[] _unitVertexPosition = new Vector4[]
        {
            new(-0.5f, -0.5f, 0.0f, 1.0f),
            new(0.5f, -0.5f, 0.0f, 1.0f),
            new(0.5f, 0.5f, 0.0f, 1.0f),
            new(-0.5f, 0.5f, 0.0f, 1.0f)
        };

        /*private readonly uint[] _indices = new uint[] { 0, 1, 2, 2, 3, 0 };*/

        private VertexArray _va;

        private StreamVertexBuffer _vb;

        private StaticIndexBuffer _ib;

        private BatchedSpriteMaterial _material;

        private int _maxBatchedSpriteCount;

        public SpriteBatcher(int maxBatchedSpriteCount)
        {
            if (Instance == null)
            {
                Instance = this;
            }
            else
            {
#if DEBUG
                Debug.Log("SpriteBatch should be unique!");
#endif
            }

            _maxBatchedSpriteCount = maxBatchedSpriteCount;

            _textures = new Texture[32];

            _va = new VertexArray();

            VertexBufferLayout layout = new VertexBufferLayout();
            layout.PushFloat(3);
            layout.PushFloat(2);
            layout.PushFloat(4);
            layout.PushFloat(1);

            _vb = new StreamVertexBuffer(_maxBatchedSpriteCount * 4 * layout.GetStride() * sizeof(float));

            _vertexData = new float[_maxBatchedSpriteCount * 4 * layout.GetStride()];

            _va.AddBuffer(layout);

            for (uint i = 0; i < _maxBatchedSpriteCount; i++)
            {
                _indexData.Add(i * 4);
                _indexData.Add(i * 4 + 1);
                _indexData.Add(i * 4 + 2);
                _indexData.Add(i * 4 + 2);
                _indexData.Add(i * 4 + 3);
                _indexData.Add(i * 4);
            }

            _ib = new StaticIndexBuffer(_indexData.ToArray());

            _material = new BatchedSpriteMaterial();
        }

        ~SpriteBatcher()
        {
            Dispose(false);
        }

        public void AddRenderer(BatchedSpriteRenderer renderer)
        {
            _rendererList.Add(renderer);
        }

        public void RemoveRenderer(BatchedSpriteRenderer renderer)
        {
            _rendererList.Remove(renderer);
        }

        public void Render()
        {
            if (_rendererList.Count == 0)
            {
                return;
            }

            SortSpritesByOrderAndZ();

            int count = 0;
            for (int i = 0; i < _rendererList.Count; i++)
            {
                if (_textureIndex >= _textures.Length || count >= _maxBatchedSpriteCount)
                {
                    Flush();
                    count = 0;
                }

                if (IndexOfTexture(_rendererList[i].sprite.texture) == -1)
                {
                    _textures[_textureIndex] = _rendererList[i].sprite.texture;
                    _textureIndex++;
                }

                AddVertexData(_rendererList[i]);

                count++;
            }

            Flush();
        }

        private int IndexOfTexture(Texture texture)
        {
            for (int i = 0; i < _textureIndex; i++)
            {
                if (_textures[i] == texture)
                {
                    return i;
                }
            }

            return -1;
        }

        private void SortSpritesByOrderAndZ()
        {
            _rendererList.Sort((a, b) =>
            {
                int result = a.orderInLayer.CompareTo(b.orderInLayer);
                if (result == 0)
                {
                    result = a.transform.position.z.CompareTo(b.transform.position.z);
                }
                return result;
            });
        }

        private void AddVertexData(BatchedSpriteRenderer renderer)
        {
            for (int i = 0; i < 4; i++)
            {
                Vector4 position = _unitVertexPosition[i] * renderer.transform.localToWorldMatrix;
                _vertexData[_vertexIndex + i * 10] = position.X;
                _vertexData[_vertexIndex + i * 10 + 1] = position.Y;
                _vertexData[_vertexIndex + i * 10 + 2] = position.Z;
                _vertexData[_vertexIndex + i * 10 + 3] = renderer.sprite.texCoords[i].x;
                _vertexData[_vertexIndex + i * 10 + 4] = renderer.sprite.texCoords[i].y;
                _vertexData[_vertexIndex + i * 10 + 5] = renderer.color.r;
                _vertexData[_vertexIndex + i * 10 + 6] = renderer.color.g;
                _vertexData[_vertexIndex + i * 10 + 7] = renderer.color.b;
                _vertexData[_vertexIndex + i * 10 + 8] = renderer.color.a;
                _vertexData[_vertexIndex + i * 10 + 9] = IndexOfTexture(renderer.sprite.texture);
            }
            _vertexIndex += 40;
        }

        private void Flush()
        {
            _va.Bind();
            _vb.SetData(_vertexData);
            _ib.Bind();
            _material.SetTextures(_textures[.._textureIndex]);
            _material.Bind();
            _material.SetUniform(Camera.main.worldToCameraMatrix);

            GL.DrawElements(PrimitiveType.Triangles, _ib.GetCount(), DrawElementsType.UnsignedInt, 0);

            _vertexIndex = 0;
            _textureIndex = 0;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }

            if (disposing)
            {
                _textures = null;
                _vertexData = null;
                _indexData = null;
            }

            _va.Dispose();
            _vb.Dispose();
            _ib.Dispose();
            _material.Dispose();

            _disposed = true;
        }
    }
}


